
package fun.sssdnsy;

import fun.sssdnsy.media.image.EmbeddedLinkedImage;
import fun.sssdnsy.media.image.LinkedImage;
import fun.sssdnsy.media.image.SecureLinkedImage;
import fun.sssdnsy.richtext.*;
import fun.sssdnsy.util.ConfigUtil;
import fun.sssdnsy.util.CryptoUtil;
import fun.sssdnsy.util.FaceRecognitionUtil;
import fun.sssdnsy.util.I18nUtil;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.input.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import javafx.stage.Screen;
import javafx.stage.Stage;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.GenericStyledArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.model.*;
import org.reactfx.SuspendableNo;
import org.reactfx.util.Either;
import org.reactfx.util.Tuple2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.io.*;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.IntFunction;

import static fun.sssdnsy.util.CryptoUtil.ALGORITHM_SM4;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;

public class Application extends javafx.application.Application {

    // the saved/loaded files and their format are arbitrary and may change across versions
    private static final String RTFX_FILE_EXTENSION = ".mypwd";
    private static final String USER_KEY = ConfigUtil.get("encryptionKey");
    private static final Logger log = LoggerFactory.getLogger(Application.class);

    private Stage mainStage;

    private final SuspendableNo updatingToolbar = new SuspendableNo();

    private final FoldableStyledArea area = new FoldableStyledArea();

    private Stage cameraStage;
    private AtomicBoolean cameraRunning = new AtomicBoolean(false);


    {
        area.setEditable(true);
        area.setWrapText(true);
        area.setStyleCodecs(ParStyle.CODEC,
                Codec.styledSegmentCodec(Codec.eitherCodec(
                        Codec.STRING_CODEC,
                        LinkedImage.codec()
                ), TextStyle.CODEC));
        area.setParagraphGraphicFactory(new BulletFactory(area));  // and folded paragraph indicator
        area.setContextMenu(new DefaultContextMenu());

        IntFunction<Node> numberFactory = LineNumberFactory.get(area);
        IntFunction<Node> arrowFactory = new ArrowFactory(area.currentParagraphProperty());
        IntFunction<Node> graphicFactory = line -> {
            HBox hbox = new HBox(
                    numberFactory.apply(line),
                    arrowFactory.apply(line));
            hbox.setAlignment(Pos.CENTER_LEFT);
            return hbox;
        };
        area.setParagraphGraphicFactory(graphicFactory);
        // 加密工具初始化
        CryptoUtil.init();
    }

    public static void main(String[] args) {
        // The following properties are required on Linux for improved text rendering
        //System.setProperty("prism.lcdtext", "false");
        //System.setProperty("prism.text", "t2k");
        launch(args);
    }


    @Override
    public void start(Stage primaryStage) {
        mainStage = primaryStage;

        Button loadBtn = createButton("loadfile", this::loadDocument, "Load document.\n\n" + "Note: the demo will load only previously-saved \"" + RTFX_FILE_EXTENSION + "\" files. " + "This file format is abitrary and may change across versions.");
        Button saveBtn = createButton("savefile", this::saveDocument, "Save document.\n\n" + "Note: the demo will save the area's content to a \"" + RTFX_FILE_EXTENSION + "\" file. " + "This file format is abitrary and may change across versions.");
        CheckBox wrapToggle = new CheckBox("Wrap");
        wrapToggle.setSelected(true);
        area.wrapTextProperty().bind(wrapToggle.selectedProperty());
        Button undoBtn = createButton("undo", area::undo, "Undo");
        Button redoBtn = createButton("redo", area::redo, "Redo");
        Button cutBtn = createButton("cut", area::cut, "Cut");
        Button copyBtn = createButton("copy", area::copy, "Copy");
        Button pasteBtn = createButton("paste", area::paste, "Paste");
        Button boldBtn = createButton("bold", this::toggleBold, "Bold");
        Button italicBtn = createButton("italic", this::toggleItalic, "Italic");
        Button underlineBtn = createButton("underline", this::toggleUnderline, "Underline");
        Button strikeBtn = createButton("strikethrough", this::toggleStrikethrough, "Strike Trough");
        Button insertImageBtn = createButton("insertimage", this::insertImage, "Insert Image");
        Button pasteSecureBtn = createButton("insertimage", this::pasteImageFromClipboard, "paste Secure Image");
        Button increaseIndentBtn = createButton("increaseIndent", this::increaseIndent, "Increase indent");
        Button decreaseIndentBtn = createButton("decreaseIndent", this::decreaseIndent, "Decrease indent");

        ToggleGroup alignmentGrp = new ToggleGroup();
        ToggleButton alignLeftBtn = createToggleButton(alignmentGrp, "align-left", this::alignLeft, "Align left");
        ToggleButton alignCenterBtn = createToggleButton(alignmentGrp, "align-center", this::alignCenter, "Align center");
        ToggleButton alignRightBtn = createToggleButton(alignmentGrp, "align-right", this::alignRight, "Align right");
        ToggleButton alignJustifyBtn = createToggleButton(alignmentGrp, "align-justify", this::alignJustify, "Justify");
        ColorPicker paragraphBackgroundPicker = new ColorPicker();
        ComboBox<Integer> sizeCombo = new ComboBox<>(FXCollections.observableArrayList(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 28, 32, 36, 40, 48, 56, 64, 72));
        sizeCombo.getSelectionModel().select(Integer.valueOf(13));
        sizeCombo.setTooltip(new Tooltip("Font size"));
        // ComboBox<String> familyCombo = new ComboBox<>(FXCollections.observableList(Font.getFamilies()));
        ComboBox<String> familyCombo = new ComboBox<>(FXCollections.observableList(
                Arrays.asList("Microsoft YaHei", "Arial", "Verdana", "Helvetica Neue", "Liberation Sans")
        ));
        familyCombo.getSelectionModel().select("Microsoft YaHei");
        familyCombo.setTooltip(new Tooltip("Font family"));
        ColorPicker textColorPicker = new ColorPicker(Color.BLACK);
        ColorPicker backgroundColorPicker = new ColorPicker();

        paragraphBackgroundPicker.setTooltip(new Tooltip("Paragraph background"));
        textColorPicker.setTooltip(new Tooltip("Text color"));
        backgroundColorPicker.setTooltip(new Tooltip("Text background"));

        paragraphBackgroundPicker.valueProperty().addListener((o, old, color) -> updateParagraphBackground(color));
        sizeCombo.setOnAction(evt -> updateFontSize(sizeCombo.getValue()));
        familyCombo.setOnAction(evt -> updateFontFamily(familyCombo.getValue()));
        textColorPicker.valueProperty().addListener((o, old, color) -> updateTextColor(color));
        backgroundColorPicker.valueProperty().addListener((o, old, color) -> updateBackgroundColor(color));

        undoBtn.disableProperty().bind(area.undoAvailableProperty().map(x -> !x));
        redoBtn.disableProperty().bind(area.redoAvailableProperty().map(x -> !x));

        BooleanBinding selectionEmpty = new BooleanBinding() {
            {
                bind(area.selectionProperty());
            }

            @Override
            protected boolean computeValue() {
                return area.getSelection().getLength() == 0;
            }
        };

        cutBtn.disableProperty().bind(selectionEmpty);
        copyBtn.disableProperty().bind(selectionEmpty);

        updateProperty(boldBtn, italicBtn, underlineBtn, strikeBtn, alignmentGrp, alignLeftBtn, alignCenterBtn, alignRightBtn, alignJustifyBtn, paragraphBackgroundPicker, sizeCombo, familyCombo, textColorPicker, backgroundColorPicker);

        ToolBar toolBar1 = new ToolBar(loadBtn, saveBtn, new Separator(Orientation.VERTICAL), wrapToggle, new Separator(Orientation.VERTICAL), undoBtn, redoBtn, new Separator(Orientation.VERTICAL), cutBtn, copyBtn, pasteBtn, new Separator(Orientation.VERTICAL), boldBtn, italicBtn, underlineBtn, strikeBtn, new Separator(Orientation.VERTICAL), alignLeftBtn, alignCenterBtn, alignRightBtn, alignJustifyBtn, new Separator(Orientation.VERTICAL), increaseIndentBtn, decreaseIndentBtn, new Separator(Orientation.VERTICAL), insertImageBtn, new Separator(Orientation.VERTICAL), pasteSecureBtn, new Separator(Orientation.VERTICAL), paragraphBackgroundPicker);

        ToolBar toolBar2 = new ToolBar(sizeCombo, familyCombo, textColorPicker, backgroundColorPicker);

        VirtualizedScrollPane<GenericStyledArea<ParStyle, Either<String, LinkedImage>, TextStyle>> vsPane = new VirtualizedScrollPane<>(area);
        VBox vbox = new VBox();
        VBox.setVgrow(vsPane, Priority.ALWAYS);
        vbox.getChildren().addAll(toolBar1, toolBar2, vsPane);
        Scene scene = initWindowSize(primaryStage, vbox);
        scene.getStylesheets().add(Application.class.getResource("/fun/sssdnsy/richtext/rich-text.css").toExternalForm());
        primaryStage.setScene(scene);
        area.requestFocus();

        // 添加快捷键处理
        addEventHandler(scene);
        primaryStage.setTitle(I18nUtil.getString("app.title"));
        primaryStage.show();

    }


    private void addEventHandler(Scene scene) {
        // 拦截 Ctrl+V 事件
        area.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            if (event.isShortcutDown() && event.getCode() == KeyCode.V) {
                Clipboard clipboard = Clipboard.getSystemClipboard();
                if (clipboard.hasImage()) {
                    pasteImageFromClipboard();
                    event.consume(); // 仅拦截图片粘贴
                } else {
                    // 允许默认文本粘贴逻辑执行
                    area.paste();
                    event.consume(); // 避免重复处理
                }
                //滚动条跟随
                area.requestFollowCaret();

            }
        });
        KeyCombination saveShortcut = new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN);
        KeyCombination openShortcut = new KeyCodeCombination(KeyCode.O, KeyCombination.SHORTCUT_DOWN);
        KeyCombination italicShortcut = new KeyCodeCombination(KeyCode.I, KeyCombination.SHORTCUT_DOWN);
        KeyCombination underlineShortcut = new KeyCodeCombination(KeyCode.U, KeyCombination.SHORTCUT_DOWN);
        KeyCombination strikethroughShortcut = new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN);
        KeyCombination boldShortcut = new KeyCodeCombination(KeyCode.B, KeyCombination.SHORTCUT_DOWN);
        KeyCombination deletelineShortcut = new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN);
        ObservableMap<KeyCombination, Runnable> accelerators = scene.getAccelerators();
        // 绑定快捷键
        accelerators.put(saveShortcut, this::saveDocument);
        accelerators.put(openShortcut, this::loadDocument);
        accelerators.put(italicShortcut, this::toggleItalic);
        accelerators.put(underlineShortcut, this::toggleUnderline);
        accelerators.put(strikethroughShortcut, this::toggleStrikethrough);
        accelerators.put(boldShortcut, this::toggleBold);

        // 其他组合键...

    }

    private void updateProperty(Button boldBtn, Button italicBtn, Button underlineBtn, Button strikeBtn, ToggleGroup alignmentGrp, ToggleButton alignLeftBtn, ToggleButton alignCenterBtn, ToggleButton alignRightBtn, ToggleButton alignJustifyBtn, ColorPicker paragraphBackgroundPicker, ComboBox<Integer> sizeCombo, ComboBox<String> familyCombo, ColorPicker textColorPicker, ColorPicker backgroundColorPicker) {
        area.beingUpdatedProperty().addListener((o, old, beingUpdated) -> {
            if (!beingUpdated) {
                boolean bold, italic, underline, strike;
                Integer fontSize;
                String fontFamily;
                Color textColor;
                Color backgroundColor;

                IndexRange selection = area.getSelection();
                if (selection.getLength() != 0) {
                    StyleSpans<TextStyle> styles = area.getStyleSpans(selection);
                    bold = styles.styleStream().anyMatch(s -> s.bold.orElse(false));
                    italic = styles.styleStream().anyMatch(s -> s.italic.orElse(false));
                    underline = styles.styleStream().anyMatch(s -> s.underline.orElse(false));
                    strike = styles.styleStream().anyMatch(s -> s.strikethrough.orElse(false));
                    int[] sizes = styles.styleStream().mapToInt(s -> s.fontSize.orElse(-1)).distinct().toArray();
                    fontSize = sizes.length == 1 ? sizes[0] : -1;
                    String[] families = styles.styleStream().map(s -> s.fontFamily.orElse(null)).distinct().toArray(String[]::new);
                    fontFamily = families.length == 1 ? families[0] : null;
                    Color[] colors = styles.styleStream().map(s -> s.textColor.orElse(null)).distinct().toArray(Color[]::new);
                    textColor = colors.length == 1 ? colors[0] : null;
                    Color[] backgrounds = styles.styleStream().map(s -> s.backgroundColor.orElse(null)).distinct().toArray(i -> new Color[i]);
                    backgroundColor = backgrounds.length == 1 ? backgrounds[0] : null;
                } else {
                    int p = area.getCurrentParagraph();
                    int col = area.getCaretColumn();
                    TextStyle style = area.getStyleAtPosition(p, col);
                    bold = style.bold.orElse(false);
                    italic = style.italic.orElse(false);
                    underline = style.underline.orElse(false);
                    strike = style.strikethrough.orElse(false);
                    fontSize = style.fontSize.orElse(-1);
                    fontFamily = style.fontFamily.orElse(null);
                    textColor = style.textColor.orElse(null);
                    backgroundColor = style.backgroundColor.orElse(null);
                }

                int startPar = area.offsetToPosition(selection.getStart(), Forward).getMajor();
                int endPar = area.offsetToPosition(selection.getEnd(), Backward).getMajor();
                List<Paragraph<ParStyle, Either<String, LinkedImage>, TextStyle>> pars = area.getParagraphs().subList(startPar, endPar + 1);

                @SuppressWarnings("unchecked") Optional<TextAlignment>[] alignments = pars.stream().map(p -> p.getParagraphStyle().alignment).distinct().toArray(Optional[]::new);
                Optional<TextAlignment> alignment = alignments.length == 1 ? alignments[0] : Optional.empty();

                @SuppressWarnings("unchecked") Optional<Color>[] paragraphBackgrounds = pars.stream().map(p -> p.getParagraphStyle().backgroundColor).distinct().toArray(Optional[]::new);
                Optional<Color> paragraphBackground = paragraphBackgrounds.length == 1 ? paragraphBackgrounds[0] : Optional.empty();

                updatingToolbar.suspendWhile(() -> {
                    btnStyleClass(boldBtn, italicBtn, bold, italic);

                    btnStyleClass(underlineBtn, strikeBtn, underline, strike);

                    if (alignment.isPresent()) {
                        TextAlignment al = alignment.get();
                        switch (al) {
                            case LEFT:
                                alignmentGrp.selectToggle(alignLeftBtn);
                                break;
                            case CENTER:
                                alignmentGrp.selectToggle(alignCenterBtn);
                                break;
                            case RIGHT:
                                alignmentGrp.selectToggle(alignRightBtn);
                                break;
                            case JUSTIFY:
                                alignmentGrp.selectToggle(alignJustifyBtn);
                                break;
                        }
                    } else {
                        alignmentGrp.selectToggle(null);
                    }

                    paragraphBackgroundPicker.setValue(paragraphBackground.orElse(null));

                    if (fontSize != -1) {
                        sizeCombo.getSelectionModel().select(fontSize);
                    } else {
                        sizeCombo.getSelectionModel().clearSelection();
                    }

                    if (fontFamily != null) {
                        familyCombo.getSelectionModel().select(fontFamily);
                    } else {
                        familyCombo.getSelectionModel().clearSelection();
                    }

                    if (textColor != null) {
                        textColorPicker.setValue(textColor);
                    }

                    backgroundColorPicker.setValue(backgroundColor);
                });
            }
        });
    }

    /**
     * 计算窗口尺寸
     */
    private Scene initWindowSize(Stage stage, VBox vbox) {
        // 获取主屏幕尺寸
        Screen screen = Screen.getPrimary();
        Rectangle2D bounds = screen.getVisualBounds();

        // 计算窗口尺寸（屏幕的68%）
        double width = bounds.getWidth() * 0.68;
        double height = bounds.getHeight() * 0.68;

        // 设置窗口尺寸和居中
        stage.setWidth(width);
        stage.setHeight(height);
        stage.centerOnScreen(); // 自动居中

        Scene scene = new Scene(vbox, width, height); // 场景尺寸匹配窗口
        stage.setScene(scene);
        return scene;
    }


    private void btnStyleClass(Button boldBtn, Button italicBtn, boolean bold, boolean italic) {
        if (bold) {
            if (!boldBtn.getStyleClass().contains("pressed")) {
                boldBtn.getStyleClass().add("pressed");
            }
        } else {
            boldBtn.getStyleClass().remove("pressed");
        }

        if (italic) {
            if (!italicBtn.getStyleClass().contains("pressed")) {
                italicBtn.getStyleClass().add("pressed");
            }
        } else {
            italicBtn.getStyleClass().remove("pressed");
        }
    }


    private Button createButton(String styleClass, Runnable action, String toolTip) {
        Button button = new Button();
        button.getStyleClass().add(styleClass);
        button.setOnAction(evt -> {
            action.run();
            area.requestFocus();
        });
        button.setPrefWidth(25);
        button.setPrefHeight(25);
        if (toolTip != null) {
            button.setTooltip(new Tooltip(toolTip));
        }
        return button;
    }

    private ToggleButton createToggleButton(ToggleGroup grp, String styleClass, Runnable action, String toolTip) {
        ToggleButton button = new ToggleButton();
        button.setToggleGroup(grp);
        button.getStyleClass().add(styleClass);
        button.setOnAction(evt -> {
            action.run();
            area.requestFocus();
        });
        button.setPrefWidth(25);
        button.setPrefHeight(25);
        if (toolTip != null) {
            button.setTooltip(new Tooltip(toolTip));
        }
        return button;
    }

    private void toggleBold() {
        updateStyleInSelection(spans -> TextStyle.bold(!spans.styleStream().allMatch(style -> style.bold.orElse(false))));
    }

    private void toggleItalic() {
        updateStyleInSelection(spans -> TextStyle.italic(!spans.styleStream().allMatch(style -> style.italic.orElse(false))));
    }

    private void toggleUnderline() {
        updateStyleInSelection(spans -> TextStyle.underline(!spans.styleStream().allMatch(style -> style.underline.orElse(false))));
    }

    private void toggleStrikethrough() {
        updateStyleInSelection(spans -> TextStyle.strikethrough(!spans.styleStream().allMatch(style -> style.strikethrough.orElse(false))));
    }

    private void alignLeft() {
        updateParagraphStyleInSelection(ParStyle.alignLeft());
    }

    private void alignCenter() {
        updateParagraphStyleInSelection(ParStyle.alignCenter());
    }

    private void alignRight() {
        updateParagraphStyleInSelection(ParStyle.alignRight());
    }

    private void alignJustify() {
        updateParagraphStyleInSelection(ParStyle.alignJustify());
    }

    private void loadDocument() {
        String userDir = System.getProperty("user.dir");
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Load document");
        String encryptFilepath = ConfigUtil.get(ConfigUtil.ENCRYPT_FILE_PATH);
        File initFile = new File(encryptFilepath).getParentFile();
        initFile = initFile == null || !initFile.isDirectory() ? new File(userDir) : initFile;
        fileChooser.setInitialDirectory(initFile);
        fileChooser.setSelectedExtensionFilter(new FileChooser
                .ExtensionFilter("Arbitrary RTFX file", "*" + RTFX_FILE_EXTENSION)
        );
        File selectedFile = fileChooser.showOpenDialog(mainStage);
        if (selectedFile != null) {
            area.clear();
            load(selectedFile);
        }
    }

    private void load(File file) {
        if ("yes".equals(ConfigUtil.get(ConfigUtil.FACE_RECONIZE, "no"))) {
            FaceRecognitionUtil.verifyWithCameraAsync(mainStage, success -> {
                if (!success) {
                    log.warn("识别失败，取消加载");
                    return;
                }
                loadFile(file);
            });
        } else {
            loadFile(file);
        }
        ConfigUtil.save(ConfigUtil.ENCRYPT_FILE_PATH, file.getAbsolutePath());
    }

    private void loadFile(File file) {
        Platform.runLater(() -> {
            // 原始文件加载逻辑
            if (area.getStyleCodecs().isPresent()) {
                Tuple2<Codec<ParStyle>, Codec<StyledSegment<Either<String, LinkedImage>, TextStyle>>> codecs = area.getStyleCodecs().get();
                Codec<StyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle>> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, area.getSegOps());

                try {
                    // 读取加密文件
                    FileInputStream fis = new FileInputStream(file);
                    byte[] encrypted = fis.readAllBytes();
                    fis.close();

                    // 解密数据
                    byte[] decrypted = CryptoUtil.decrypt(encrypted, ALGORITHM_SM4, USER_KEY);

                    // 反序列化文档
                    ByteArrayInputStream bis = new ByteArrayInputStream(decrypted);
                    DataInputStream dis = new DataInputStream(bis);

                    StyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle> doc = codec.decode(dis);
                    if (doc != null) {
                        area.replaceSelection(doc);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }


    private void saveDocument() {
        String initialDir = System.getProperty("user.dir");
        String configFilePath = ConfigUtil.get(ConfigUtil.ENCRYPT_FILE_PATH, initialDir);
        File file = new File(configFilePath);
        File selectedFile = null;
        if (!file.exists()) {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("Save document");
            fileChooser.setInitialDirectory(file.getParentFile());
            fileChooser.setInitialFileName("data" + RTFX_FILE_EXTENSION);
            selectedFile = fileChooser.showSaveDialog(mainStage);
        } else {
            selectedFile = file;
        }

        if (selectedFile != null) {
            ConfigUtil.save(ConfigUtil.ENCRYPT_FILE_PATH, selectedFile.getAbsolutePath());
        }
        save(file);
    }


    private void save(File file) {
        StyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle> doc = area.getDocument();

        // Use the Codec to save the document in a binary format
        area.getStyleCodecs().ifPresent(codecs -> {
            Codec<StyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle>> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, area.getSegOps());
            try {
                // 序列化文档
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(bos);
                codec.encode(dos, doc);
                dos.close();

                // 加密并写入文件
                byte[] encrypted = CryptoUtil.encrypt(bos.toByteArray(), ALGORITHM_SM4, USER_KEY);
                FileOutputStream fos = new FileOutputStream(file);
                fos.write(encrypted);
                fos.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            } catch (Exception exception) {
                throw new RuntimeException(exception);
            }
        });
    }


    /**
     * Action listener which inserts a new image at the current caret position.
     */
    private void insertImage() {
//        String initialDir = System.getProperty("user.dir");
//        FileChooser fileChooser = new FileChooser();
//        fileChooser.setTitle("Insert image");
//        fileChooser.setInitialDirectory(new File(initialDir));
//        File selectedFile = fileChooser.showOpenDialog(mainStage);
//        if (selectedFile != null) {
//            String imagePath = selectedFile.getAbsolutePath();
//            imagePath = imagePath.replace('\\', '/');
//            ReadOnlyStyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle> ros = ReadOnlyStyledDocument.fromSegment(Either.right(new RealLinkedImage(imagePath)), ParStyle.EMPTY, TextStyle.EMPTY, area.getSegOps());
//            area.replaceSelection(ros);
//        }
        FileChooser fileChooser = new FileChooser();
        File selectedFile = fileChooser.showOpenDialog(mainStage);
        if (selectedFile != null) {
            try {
                byte[] imageData = Files.readAllBytes(selectedFile.toPath());
                // 插入前确保选区在文档末尾
                area.moveTo(area.getLength()); // 关键：重置插入位置
                ReadOnlyStyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle> doc =
                        ReadOnlyStyledDocument.fromSegment(
                                Either.right(new EmbeddedLinkedImage(imageData)),
                                ParStyle.EMPTY,
                                TextStyle.EMPTY,
                                area.getSegOps()
                        );
                area.replaceSelection(doc);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 粘贴图片
     */
    private void pasteImageFromClipboard() {
        log.info("[DEBUG] 开始粘贴图片，当前文档长度: " + area.getLength());
        Clipboard clipboard = Clipboard.getSystemClipboard();
        if (!clipboard.hasImage()) {
            return;
        }

        try {
//            // 获取并序列化图片
            Image image = clipboard.getImage();
            log.info("[DEBUG] 剪贴板图片尺寸: " + image.getWidth() + "x" + image.getHeight());

//            byte[] imageData = ImageSerializer.serializeImage(image, mainStage.widthProperty().multiply(0.5d), mainStage.heightProperty().multiply(0.5d));
//
//            // 保存为安全格式文件
//            String storagePath = ImageSerializer.getImageStoragePath();
//            String filename = ImageSerializer.saveToSecureFile(imageData, storagePath);
//
//            // 插入到编辑器（使用自定义URI格式）
//            insertSecureImageReference("secureimg://" + filename);

            // 确保光标位置有效
            int caretPosition = area.getCaretPosition();
            if (caretPosition < 0 || caretPosition > area.getLength()) {
                caretPosition = area.getLength(); // 若无效则移动到末尾
            }
            area.moveTo(caretPosition);
            // 转换图片为字节数组
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ImageIO.write(SwingFXUtils.fromFXImage(image, null), "PNG", bos);
            byte[] imageData = bos.toByteArray();
            log.info("[DEBUG] 图片字节长度: " + imageData.length);

            // 插入前光标位置
            int caretPos = area.getCaretPosition();
            log.info("[DEBUG] 创建文档片段并插入 插入前光标位置: " + caretPos);
            // 创建文档片段并插入
            ReadOnlyStyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle> doc =
                    ReadOnlyStyledDocument.fromSegment(
                            Either.right(new EmbeddedLinkedImage(imageData)),
                            ParStyle.EMPTY,
                            TextStyle.EMPTY,
                            area.getSegOps()
                    );

            area.replaceSelection(doc);
            // 插入后显式移动光标到新位置
            int newLength = area.getLength() + 1;
            area.moveTo(newLength);

        } catch (Exception e) {
            log.error("粘贴图片失败:");
            e.printStackTrace();
        }
    }


    private void insertSecureImageReference(String uri) {
        ReadOnlyStyledDocument<ParStyle, Either<String, LinkedImage>, TextStyle> doc = ReadOnlyStyledDocument.fromSegment(Either.right(new SecureLinkedImage(uri)), ParStyle.EMPTY, TextStyle.EMPTY, area.getSegOps());
        area.replaceSelection(doc);
    }


    private void increaseIndent() {
        updateParagraphStyleInSelection(ps -> ps.increaseIndent());
    }

    private void decreaseIndent() {
        updateParagraphStyleInSelection(ps -> ps.decreaseIndent());
    }

    private void updateStyleInSelection(Function<StyleSpans<TextStyle>, TextStyle> mixinGetter) {
        IndexRange selection = area.getSelection();
        if (selection.getLength() != 0) {
            StyleSpans<TextStyle> styles = area.getStyleSpans(selection);
            TextStyle mixin = mixinGetter.apply(styles);
            StyleSpans<TextStyle> newStyles = styles.mapStyles(style -> style.updateWith(mixin));
            area.setStyleSpans(selection.getStart(), newStyles);
        }
    }

    private void updateStyleInSelection(TextStyle mixin) {
        IndexRange selection = area.getSelection();
        if (selection.getLength() != 0) {
            StyleSpans<TextStyle> styles = area.getStyleSpans(selection);
            StyleSpans<TextStyle> newStyles = styles.mapStyles(style -> style.updateWith(mixin));
            area.setStyleSpans(selection.getStart(), newStyles);
        }
    }

    private void updateParagraphStyleInSelection(Function<ParStyle, ParStyle> updater) {
        IndexRange selection = area.getSelection();
        int startPar = area.offsetToPosition(selection.getStart(), Forward).getMajor();
        int endPar = area.offsetToPosition(selection.getEnd(), Backward).getMajor();
        for (int i = startPar; i <= endPar; ++i) {
            Paragraph<ParStyle, Either<String, LinkedImage>, TextStyle> paragraph = area.getParagraph(i);
            area.setParagraphStyle(i, updater.apply(paragraph.getParagraphStyle()));
        }
    }

    private void updateParagraphStyleInSelection(ParStyle mixin) {
        updateParagraphStyleInSelection(style -> style.updateWith(mixin));
    }

    private void updateFontSize(Integer size) {
        if (!updatingToolbar.get()) {
            updateStyleInSelection(TextStyle.fontSize(size));
        }
    }

    private void updateFontFamily(String family) {
        if (!updatingToolbar.get() && area.getSelection().getLength() > 0) {
            updateStyleInSelection(TextStyle.fontFamily(family));
        }
    }

    private void updateTextColor(Color color) {
        if (!updatingToolbar.get()) {
            updateStyleInSelection(TextStyle.textColor(color));
        }
    }

    private void updateBackgroundColor(Color color) {
        if (!updatingToolbar.get()) {
            updateStyleInSelection(TextStyle.backgroundColor(color));
        }
    }

    private void updateParagraphBackground(Color color) {
        if (!updatingToolbar.get()) {
            updateParagraphStyleInSelection(ParStyle.backgroundColor(color));
        }
    }
}
